﻿/* $RCSFile: CssHandler.cs $
 * Apache license v2 2011 by Christian Surieux
 * $Source: ?/ADNCRM.Core/HttpHandlers/CssHandler.cs $
 * $log$
 * Revision 6 2010/08/06 19:56:28  christian.surieux
 *   Added Log to All cs files
 */

using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO.Compression;
using System.Text.RegularExpressions;
using System.Web;
using System.IO;
using System.Web.Caching;
using System.Globalization;

using AddonNice.Helpers;
using AddonNice.Diagnostics;

namespace AddonNice.HttpHandlers
{
    public abstract class CompressionHandler : IHttpHandler
    {
        public abstract void ProcessRequest(HttpContext context);
        public static Stream Compress(HttpContext context,HttpResponse response,Stream stream)
        {
            if ( HttpHandlerTrace.Sw.TraceVerbose )
                Trace.WriteLine("[V]CompressionHandler Compress BEG.",HttpHandlerTrace.Sw.Info);
            const string enc            =   "Accept-Encoding";
            string encoding             =   context.Request.Headers[enc];
            Stream stream2              =   stream;
            if ( !string.IsNullOrEmpty(encoding) )
            {
                const string gzip       =   "gzip";
                const string deflate    =   "deflate";
                const string header     =   "Content-Encoding";
                if ( HttpHandlerTrace.Sw.TraceVerbose )
                    Trace.WriteLine( string.Format(CultureInfo.InvariantCulture,"[V]CompressionHandler Compress encoding: {0}.",encoding),HttpHandlerTrace.Sw.Info);
                if ( encoding.ContainsIgnoreCase(gzip) )
                {
                    response.AddHeader(header,gzip);
                    stream2             =   new GZipStream(stream,CompressionMode.Compress);
                    if ( HttpHandlerTrace.Sw.TraceVerbose )
                        Trace.WriteLine("[V]CompressionHandler Compress GZip.",HttpHandlerTrace.Sw.Info);
                }
                else if ( encoding.ContainsIgnoreCase(deflate) )
                {
                    response.AddHeader(header,deflate);
                    stream2             =   new DeflateStream(stream,CompressionMode.Compress);
                    if ( HttpHandlerTrace.Sw.TraceVerbose )
                        Trace.WriteLine("[V]CompressionHandler Compress Deflate.",HttpHandlerTrace.Sw.Info);
                }
            }
            if ( HttpHandlerTrace.Sw.TraceVerbose )
                Trace.WriteLine("[V]CompressionHandler Compress END.",HttpHandlerTrace.Sw.Info);
            return stream2;
        }
        public virtual bool IsReusable 
        { 
            get { return true; } 
        }
    }


    /// <summary>
    /// The Combine method will traverse every directory it is passed, combine the minified CSS output for any CSS file it finds in those directories, 
    /// and then cache that content by directory with file dependencies, 
    /// so that any changes to a file will invalidate its directory’s cache (but not the entire request’s style info) out to the response’s output stream. 
    /// </summary>
    public class CssHandler : CompressionHandler
    {
        public const string ContentType = "text/css";

        public override void ProcessRequest(HttpContext context)
        {
            if ( HttpHandlerTrace.Sw.TraceVerbose )
                Trace.WriteLine("[V]CssHandler ProcessRequest BEG.",HttpHandlerTrace.Sw.Info);
            // Collect the query parameters        
            string cltCache         =   context.Request.QueryString["c"];
            string theme            =   context.Request.QueryString["t"];
            string directory        =   context.Request.QueryString["d"];
            string compress         =   context.Request.QueryString["p"];
            if ( HttpHandlerTrace.Sw.TraceVerbose )
                Trace.WriteLine( string.Format(CultureInfo.InvariantCulture,"[V]CssHandler ProcessRequest cltCache: {0}, theme: {1}, directory: {2}, compress: {3}.",
                    new object[]{cltCache,theme,directory,compress}),HttpHandlerTrace.Sw.Info);
            // Identify the response as CSS        
            HttpResponse response   =   context.Response;
            response.ContentType    =   ContentType;
            // Wrap the response in a compression stream if compression on
            Stream output           =   response.OutputStream;
            if ( !string.IsNullOrEmpty(compress) )
                output              =   Compress(context,response,output);

            if ( !string.IsNullOrEmpty(theme) )
                ProcessByTheme(context,theme,output);
            if ( !string.IsNullOrEmpty(directory) )
                ProcessByDirectory(context, directory, output);
            
            // Cache our work on the clientvar 
            if ( !string.IsNullOrEmpty(cltCache) )
            {
                HttpCachePolicy clientCache     =   response.Cache;
                clientCache.SetCacheability(HttpCacheability.Public);
                clientCache.SetValidUntilExpires(true);
                clientCache.SetLastModifiedFromFileDependencies();
                clientCache.SetETagFromFileDependencies();
            }
            if ( HttpHandlerTrace.Sw.TraceVerbose )
                Trace.WriteLine("[V]CssHandler ProcessRequest END.",HttpHandlerTrace.Sw.Info);
        }

        /// <summary>
        ///  Returns the virtual file path for a specified physical file path    
        /// </summary>
        public string MapPathReverse(string path)
        {
            var context                 =   HttpContext.Current;
            if ( context != null )
            {
                if ( context.Request.PhysicalApplicationPath != null )
                {
                    string root         =   context.Request.PhysicalApplicationPath.TrimEnd('\\');
                    string relative     =   path.Replace(root,String.Empty);
                    string clean        =   relative.Replace('\\','/');
                    return clean.Insert(0,"~");
                }
            }
            return string.Empty;
        }


        private void ProcessByDirectory(HttpContext context, string directory, Stream output)
        {    
            if (!string.IsNullOrEmpty(directory) )    
            {        
                var path    =   context.Server.MapPath(directory);        
                CombineFromDirectory(context, output, path);    
            }
        }
        
        private void ProcessByTheme(HttpContext context,string theme,Stream output)
        {
            if ( !string.IsNullOrEmpty(theme) )
            {
                // Get all theme directories        
                var path    =   context.Server.MapPath(string.Format("~/App_Themes/{0}",theme));
                if ( HttpHandlerTrace.Sw.TraceVerbose )
                    Trace.WriteLine( string.Format(CultureInfo.InvariantCulture,"[V]CompressionHandler ProcessByTheme BEG path: {0}.",path),HttpHandlerTrace.Sw.Info);
                CombineFromDirectory(context,output,path);
            }
        }

        private void CombineFromDirectory(HttpContext context,Stream output,string directory)
        {
            // Get a list of all sub-directories and add the parent    
            var directories     =   Directory.GetDirectories(directory,"*",SearchOption.AllDirectories).ToList();
            directories.Add(directory);
            // Convert paths back to virtual    
            for ( int p = 0 ; p < directories.Count ; p++ )
            {
                directories[p]  =    MapPathReverse(directories[p]);
            }
            // Combine the directory manifest    
            Combine(context,output,directories);
        }

        private void Combine(HttpContext context,Stream output,string relativePath)
        {
            Combine(context,output,new[] { relativePath });
        }

        private void Combine(HttpContext context,Stream output,params string[] relativePaths)
        {
            Combine(context,output,relativePaths.ToList());
        }

        /// <summary>
        /// Content will be cached with a 1 day sliding window on server
        /// </summary>
        private void Combine(HttpContext context,Stream output,IEnumerable<string> relativePaths)
        {
            if ( HttpHandlerTrace.Sw.TraceVerbose )
                Trace.WriteLine( string.Format(CultureInfo.InvariantCulture,"[V]CompressionHandler Combine BEG Count: {0}.",relativePaths.Count()),HttpHandlerTrace.Sw.Info);
            using ( StreamWriter sw         =   new StreamWriter(output) )
            {
                // HttpRuntime is faster than HttpContext.Current        
                Cache cache                 =   HttpRuntime.Cache;
                foreach ( string relativePath in relativePaths )
                {
                    if ( HttpHandlerTrace.Sw.TraceVerbose )
                        Trace.WriteLine( string.Format(CultureInfo.InvariantCulture,"[V]CompressionHandler Combine relativePath: '{0}'",relativePath),HttpHandlerTrace.Sw.Info);
                    // Check the cache for this relative path first            
                    if ( cache[relativePath] == null )
                    {
                        StringBuilder sb    =   new StringBuilder();
                        string path         =   context.Server.MapPath(relativePath);
                        List<string> files  =   new List<string>();
                        // Test that the provided path is not a full file                
                        string pathAsFile   =   Path.GetFileName(path);
                        if ( !pathAsFile.Contains('.') )
                        {
                            files.AddRange(Directory.GetFiles(path,"*.css"));
                        }
                        else
                        {
                            // Already a file                    
                            files.Add(path);
                        }
                        foreach ( string file in files )
                        {
                            if ( HttpHandlerTrace.Sw.TraceVerbose )
                                Trace.WriteLine( string.Format(CultureInfo.InvariantCulture,"[V]CompressionHandler Combine file: '{0}'",file),HttpHandlerTrace.Sw.Info);
                            // Read the file and minify it                        
                            string minified         =  Check4Import(context,File.ReadAllText(file));
                            // Write the file to a temporary builder                        
                            sb.Append(minified);
                        }
                        // Combine the string content                
                        string content              =   sb.ToString();
                        // Create a cache dependency based on the files we just combined                
                        CacheDependency dependency  =   new CacheDependency(files.ToArray());
                        // Cache the content by path name, so we don't combine it twice                
                        cache.Insert(relativePath,content,dependency,Cache.NoAbsoluteExpiration,TimeSpan.FromDays(1));
                        sw.Write(content);
                    }
                    else
                    {
                        if ( HttpHandlerTrace.Sw.TraceVerbose )
                            Trace.WriteLine( string.Format(CultureInfo.InvariantCulture,"[V]CompressionHandler Combine found in cache relativePath: '{0}'",relativePath),HttpHandlerTrace.Sw.Info);
                        // It's already cached, so serve it up                
                        var existing                =   cache[relativePath].ToString();
                        sw.Write(existing);
                    }
                }
            }
            if ( HttpHandlerTrace.Sw.TraceVerbose )
                Trace.WriteLine( "[V]CompressionHandler Combine END.",HttpHandlerTrace.Sw.Info);
        }

        /// <summary>
        /// Search for: @import url("/App_Themes/defaultstd.css"); and insert the corersponding file
        /// </summary>
        public static string Check4Import(HttpContext context,string input)
        {
            if ( HttpHandlerTrace.Sw.TraceVerbose )
                Trace.WriteLine("[V]CompressionHandler Check4Import BEG.",HttpHandlerTrace.Sw.Info);
            const string strImport  =   @"@import\s+url\(\s*(""|\')(?<url>[^(""|\')]+)(""|\')\s*\)\s*;";
            Regex pattern           =   new Regex(strImport,RegexOptions.IgnoreCase | RegexOptions.Compiled);
            Match match             =   pattern.Match(input);
            if ( HttpHandlerTrace.Sw.TraceVerbose )
                Trace.WriteLine( string.Format(CultureInfo.InvariantCulture,"[V]CompressionHandler Check4Import match: {0}",match.Success),HttpHandlerTrace.Sw.Info);
            if ( !match.Success )
                return input;
            StringBuilder sb        =   new StringBuilder();
            int idx                 =   match.Index;
            string imprt            =   match.Groups["url"].Value;
            string path             =   context.Server.MapPath(imprt);
            if ( HttpHandlerTrace.Sw.TraceVerbose )
                Trace.WriteLine( string.Format(CultureInfo.InvariantCulture,"[V]CompressionHandler Check4Import path: '{0}'",path),HttpHandlerTrace.Sw.Info);
            string impTxt           =   File.ReadAllText(path);
            sb.Append(impTxt);
            input                   =   pattern.Replace(input,string.Empty);
            sb.Append(input);
            if ( HttpHandlerTrace.Sw.TraceVerbose )
                Trace.WriteLine("[V]CompressionHandler Check4Import END.",HttpHandlerTrace.Sw.Info);
            return sb.ToString();
        }
    }
}
